iT邦幫忙

2026 iThome 鐵人賽

DAY 12
1
Kubernetes

從零到一:使用 K8S + GitOps 打造異構技術棧的資料分析平台系列 第 12

[Day 12] 持久化存儲:在 K8S 上管理 Delta Lake 數據存儲 (PV/PVC) —— 解決資料庫與數據文件在容器重啟後的丟失問題。

  • 分享至 

  • xImage
  •  

Day 12: 持久化存儲:在 K8S 上管理 Delta Lake 數據存儲 (PV/PVC)

解決資料庫與數據文件在容器重啟後的丟失問題。

1. 容器的「失憶症」

在 Kubernetes 中,Pod 的生命週期是短暫的。如果把 Wafer BI 的 Delta Lake 大數據直接寫在 Python 容器的檔案系統裡,當 K8S 因為負載過高或節點更新把 Pod 砍掉重建時——恭喜,數千萬筆晶圓測試資料瞬間蒸發,一位元組都不剩。

Day 8 我們才炫耀過「刪 Pod 兩秒自動復活」,但復活的是全新的 Pod,裡面的資料可不會跟著轉生。

2. PV 與 PVC 的概念

K8S 用兩個資源解決這件事:

  • PV (Persistent Volume):實際的硬碟空間(例如 OCI Block Volume、NFS、本機路徑)
  • PVC (Persistent Volume Claim):應用程式對存儲空間的「請款單」——我要 50Gi、要能讀寫

應用只管開請款單,至於硬碟從哪來,交給 StorageClass 動態配置。這個抽象讓同一份 YAML 在不同環境都能跑而不用改應用——本機 Docker Desktop 預設是 hostpath,換成雲端託管的 CSI 驅動(例如 OCI 的 oci-bv)也一樣,換的只是 StorageClass,PVC 宣告本身不用動。

除了「要多大空間」,PVC 請款單上還有一欄容易被忽略:accessModes——宣告這塊空間允許怎麼被掛載。K8S 定義了三種:

  • ReadWriteOnce (RWO):同一時間只允許一個 Node 以讀寫模式掛載
  • ReadOnlyMany (ROX):可以同時被多個 Node 掛載,但都只能讀
  • ReadWriteMany (RWX):可以同時被多個 Node 讀寫掛載——但需要 NFS、雲端檔案儲存這類特殊後端支援,hostPathlocal-path 完全不支援這個模式

這個專案實際用哪一種? 目前 Helm Chart 裡真的有掛 PVC 的,是 postgresai-mcp-service(存放向量資料庫 ChromaDB 的資料),兩者都宣告 accessModes: [ReadWriteOnce],也都沒有指定 storageClassName,直接吃叢集預設值。選 RWO 的原因很單純:這兩個服務都只開 1 個副本,是典型的單體 stateful 服務,沒有「好幾個 Pod 同時要讀寫同一份資料」的需求,RWO 已經夠用;反而是硬要上 RWX,會是不必要的過度設計(下面會解釋為什麼)。

跟 Docker 的 bind mount 是不是同一件事? 概念上有關聯,但不是同一層抽象。Docker 的 bind mount(-v /host/path:/container/path)是直接把「主機上的某個路徑」掛進容器,沒有任何中間層——容器因此綁死那台特定主機的檔案系統佈局,換一台機器路徑不對就掛不起來。K8S 的 PV/PVC 反過來設計:應用先宣告「我要多少空間、要什麼讀寫模式」(PVC),至於用什麼東西去滿足這個請求,交給 StorageClass 動態決定(PV)。另外,K8S 的 hostPath PV 類型,底層做的事情其實跟 Docker bind mount 一模一樣(把 Node 上某個路徑掛進 Pod),只是外面包了一層 PVC 的可攜式介面——所以本機 Docker Desktop 用的 hostpath StorageClass,說穿了就是「K8S 化的 bind mount」,差別在於應用程式的 YAML 完全不知道底層是 bind mount,只認得那張「請款單」。

兩個 Pod 共用一份 Volume,要怎麼避免同時寫入互相搞壞? 這裡有幾層要拆開看:

  1. RWO 常被誤解:很多人以為 ReadWriteOnce 是「只有一個 Pod 能掛」,官方定義其實是「只有一個 Node 能以讀寫模式掛載」——如果兩個 Pod 剛好被排程到同一台 Node 上,K8S 並不會阻止它們同時掛上同一個 RWO Volume。
  2. 真的要跨 Node 共用,要用 ReadWriteMany (RWX),但這需要 NFS、雲端檔案儲存(例如 OCI File Storage)這類支援 RWX 的後端,hostPathlocal-path 完全不支援。
  3. 關鍵:K8S 只負責「讓不讓掛」,完全不管「掛上去之後兩個 Pod 寫同一個檔案會不會衝突」——這是應用層自己要處理的事,常見做法:
    • 分區寫入:每個 Pod 只碰自己專屬的檔案或子目錄(檔名帶 Pod 名稱),彼此的寫入範圍天生不重疊
    • 單一寫入者模式:多個 Pod 都能讀,但只有一個 Pod(例如 StatefulSet 的 pod-0,或選出來的 leader)有寫入權限
    • 應用層加鎖:用 flock/fcntl 之類的 advisory lock,但要注意 NFS 上的鎖行為不一定可靠,得看後端實作
    • 靠有交易保護的儲存格式:Day 4 提過 Delta Lake 的 _delta_log/ 用原子性的 commit 檔案協調寫入,就算真的有多個行程動到同一份資料,靠的也是這層交易日誌本身,而不是期待底層檔案系統幫你處理併發

postgres 這種傳統關聯式資料庫,答案其實更直接:不要讓兩個 Postgres 行程指向同一份資料目錄。這不是「怎麼避免衝突」的問題,是 Postgres 從設計上就不支援多行程共寫同一份資料檔案,兩個實例搶同一個 data directory 只會直接把資料庫弄壞。這也是為什麼 postgres-pvc 就是單純的 RWO + 單一副本,從一開始就沒有考慮 RWX 這條路。

3. 實際配置

建立 PVC,並掛載到容器的 /app/wafer_delta_table

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wafer-delta-pvc
  namespace: k8sdemo
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 50Gi   # 本機示範用 1Gi 就夠
      containers:
      - name: backend
        image: ghcr.io/darkschneider1024/wafer-bi-backend:latest
        volumeMounts:
        - name: delta-storage
          mountPath: /app/wafer_delta_table
      volumes:
      - name: delta-storage
        persistentVolumeClaim:
          claimName: wafer-delta-pvc

4. 實測:刪 Pod,資料還在嗎?

口說無憑,直接做給你看。流程:掛載 PVC → 生成 6.8MB 的 Delta Lake 資料 → 砍掉 Pod → 到新 Pod 裡看資料還在不在:

https://ithelp.ithome.com.tw/upload/images/20260814/20182549CBiRDwJHVY.png

▲ PVC 持久化實測:刪除 Pod 後,新 Pod 裡的 Delta Lake 資料一位元組都沒少

重點看最後一段:Pod 名字從 8r2j2 換成了 4b5g5(貨真價實的新 Pod),但 /app/wafer_delta_table 裡的 _delta_log 和 parquet 檔完好如初,du 還是那個 6.8M。

一個小細節:PVC 掛上去的瞬間,掛載點會遮蔽 (shadow) image 裡原本烘進去的資料,所以第一次啟動時目錄是空的,要重新生成或搬移資料進去。這個行為跟 Linux 的 mount 一模一樣,第一次遇到很容易愣住(我就愣了幾分鐘,還以為資料被吃了 哈哈)。

5. 小結

現在就算 Python Pod 被 K8S 重啟一百次,晶圓數據也安然無恙。但還有一類東西不能寫死在 image 裡——資料庫密碼、JWT 密鑰、環境設定。明天來聊 ConfigMap 與 Secret 的安全實踐。


上一篇
[Day 11] 流量控制:Nginx Ingress Controller 與 SSL 憑證設定 —— 讓外部流量安全、精準地導向正確的微服務。
下一篇
[Day 13] 配置管理:Secrets 與 ConfigMaps 的安全實踐 —— 將敏感資訊與環境變數從代碼中抽離,實現安全管理。
系列文
從零到一:使用 K8S + GitOps 打造異構技術棧的資料分析平台17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言